// XMLHttpRequest.js Copyright (C) 2011 Sergey Ilinsky (http://www.ilinsky.com)
//
// This work is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.

// This work is distributed in the hope that it will be useful,
// but without any warranty; without even the implied warranty of
// merchantability or fitness for a particular purpose. See the
// GNU Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

(function () {

  // Save reference to earlier defined object implementation (if any)
  var oXMLHttpRequest = window.XMLHttpRequest;

  // Define on browser type
  var bGecko  = !!window.controllers,
      bIE     = window.document.all && !window.opera,
      bIE7    = bIE && window.navigator.userAgent.match(/MSIE 7.0/);

  // Enables "XMLHttpRequest()" call next to "new XMLHttpReques()"
  function fXMLHttpRequest() {
    this._object  = oXMLHttpRequest && !bIE7 ? new oXMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
    this._listeners = [];
  };

  // Constructor
  function cXMLHttpRequest() {
    return new fXMLHttpRequest;
  };
  cXMLHttpRequest.prototype = fXMLHttpRequest.prototype;

  // BUGFIX: Firefox with Firebug installed would break pages if not executed
  if (bGecko && oXMLHttpRequest.wrapped)
    cXMLHttpRequest.wrapped = oXMLHttpRequest.wrapped;

  // Constants
  cXMLHttpRequest.UNSENT            = 0;
  cXMLHttpRequest.OPENED            = 1;
  cXMLHttpRequest.HEADERS_RECEIVED  = 2;
  cXMLHttpRequest.LOADING           = 3;
  cXMLHttpRequest.DONE              = 4;

  // Public Properties
  cXMLHttpRequest.prototype.readyState    = cXMLHttpRequest.UNSENT;
  cXMLHttpRequest.prototype.responseText  = '';
  cXMLHttpRequest.prototype.responseXML   = null;
  cXMLHttpRequest.prototype.status        = 0;
  cXMLHttpRequest.prototype.statusText    = '';

  // Priority proposal
  cXMLHttpRequest.prototype.priority    = "NORMAL";

  // Instance-level Events Handlers
  cXMLHttpRequest.prototype.onreadystatechange  = null;

  // Class-level Events Handlers
  cXMLHttpRequest.onreadystatechange  = null;
  cXMLHttpRequest.onopen              = null;
  cXMLHttpRequest.onsend              = null;
  cXMLHttpRequest.onabort             = null;

  // Public Methods
  cXMLHttpRequest.prototype.open  = function(sMethod, sUrl, bAsync, sUser, sPassword) {
    // Delete headers, required when object is reused
    delete this._headers;

    // When bAsync parameter value is omitted, use true as default
    if (arguments.length < 3)
      bAsync  = true;

    // Save async parameter for fixing Gecko bug with missing readystatechange in synchronous requests
    this._async   = bAsync;

    // Set the onreadystatechange handler
    var oRequest  = this,
      nState    = this.readyState,
      fOnUnload;

    // BUGFIX: IE - memory leak on page unload (inter-page leak)
    if (bIE && bAsync) {
      fOnUnload = function() {
        if (nState != cXMLHttpRequest.DONE) {
          fCleanTransport(oRequest);
          // Safe to abort here since onreadystatechange handler removed
          oRequest.abort();
        }
      };
      window.attachEvent("onunload", fOnUnload);
    }

    // Add method sniffer
    if (cXMLHttpRequest.onopen)
      cXMLHttpRequest.onopen.apply(this, arguments);

    if (arguments.length > 4)
      this._object.open(sMethod, sUrl, bAsync, sUser, sPassword);
    else
    if (arguments.length > 3)
      this._object.open(sMethod, sUrl, bAsync, sUser);
    else
      this._object.open(sMethod, sUrl, bAsync);

    this.readyState = cXMLHttpRequest.OPENED;
    fReadyStateChange(this);

    this._object.onreadystatechange = function() {
      if (bGecko && !bAsync)
        return;

      // Synchronize state
      oRequest.readyState   = oRequest._object.readyState;

      //
      fSynchronizeValues(oRequest);

      // BUGFIX: Firefox fires unnecessary DONE when aborting
      if (oRequest._aborted) {
        // Reset readyState to UNSENT
        oRequest.readyState = cXMLHttpRequest.UNSENT;

        // Return now
        return;
      }

      if (oRequest.readyState == cXMLHttpRequest.DONE) {
        // Free up queue
        delete oRequest._data;
/*        if (bAsync)
          fQueue_remove(oRequest);*/
        //
        fCleanTransport(oRequest);
// Uncomment this block if you need a fix for IE cache
/*
        // BUGFIX: IE - cache issue
        if (!oRequest._object.getResponseHeader("Date")) {
          // Save object to cache
          oRequest._cached  = oRequest._object;

          // Instantiate a new transport object
          cXMLHttpRequest.call(oRequest);

          // Re-send request
          if (sUser) {
            if (sPassword)
              oRequest._object.open(sMethod, sUrl, bAsync, sUser, sPassword);
            else
              oRequest._object.open(sMethod, sUrl, bAsync, sUser);
          }
          else
            oRequest._object.open(sMethod, sUrl, bAsync);
          oRequest._object.setRequestHeader("If-Modified-Since", oRequest._cached.getResponseHeader("Last-Modified") || new window.Date(0));
          // Copy headers set
          if (oRequest._headers)
            for (var sHeader in oRequest._headers)
              if (typeof oRequest._headers[sHeader] == "string")  // Some frameworks prototype objects with functions
                oRequest._object.setRequestHeader(sHeader, oRequest._headers[sHeader]);

          oRequest._object.onreadystatechange = function() {
            // Synchronize state
            oRequest.readyState   = oRequest._object.readyState;

            if (oRequest._aborted) {
              //
              oRequest.readyState = cXMLHttpRequest.UNSENT;

              // Return
              return;
            }

            if (oRequest.readyState == cXMLHttpRequest.DONE) {
              // Clean Object
              fCleanTransport(oRequest);

              // get cached request
              if (oRequest.status == 304)
                oRequest._object  = oRequest._cached;

              //
              delete oRequest._cached;

              //
              fSynchronizeValues(oRequest);

              //
              fReadyStateChange(oRequest);

              // BUGFIX: IE - memory leak in interrupted
              if (bIE && bAsync)
                window.detachEvent("onunload", fOnUnload);
            }
          };
          oRequest._object.send(null);

          // Return now - wait until re-sent request is finished
          return;
        };
*/
        // BUGFIX: IE - memory leak in interrupted
        if (bIE && bAsync)
          window.detachEvent("onunload", fOnUnload);
      }

      // BUGFIX: Some browsers (Internet Explorer, Gecko) fire OPEN readystate twice
      if (nState != oRequest.readyState)
        fReadyStateChange(oRequest);

      nState  = oRequest.readyState;
    }
  };
  function fXMLHttpRequest_send(oRequest) {
    oRequest._object.send(oRequest._data);

    // BUGFIX: Gecko - missing readystatechange calls in synchronous requests
    if (bGecko && !oRequest._async) {
      oRequest.readyState = cXMLHttpRequest.OPENED;

      // Synchronize state
      fSynchronizeValues(oRequest);

      // Simulate missing states
      while (oRequest.readyState < cXMLHttpRequest.DONE) {
        oRequest.readyState++;
        fReadyStateChange(oRequest);
        // Check if we are aborted
        if (oRequest._aborted)
          return;
      }
    }
  };
  cXMLHttpRequest.prototype.send  = function(vData) {
    // Add method sniffer
    if (cXMLHttpRequest.onsend)
      cXMLHttpRequest.onsend.apply(this, arguments);

    if (!arguments.length)
      vData = null;

    // BUGFIX: Safari - fails sending documents created/modified dynamically, so an explicit serialization required
    // BUGFIX: IE - rewrites any custom mime-type to "text/xml" in case an XMLNode is sent
    // BUGFIX: Gecko - fails sending Element (this is up to the implementation either to standard)
    if (vData && vData.nodeType) {
      vData = window.XMLSerializer ? new window.XMLSerializer().serializeToString(vData) : vData.xml;
      if (!oRequest._headers["Content-Type"])
        oRequest._object.setRequestHeader("Content-Type", "application/xml");
    }

    this._data  = vData;
/*
    // Add to queue
    if (this._async)
      fQueue_add(this);
    else*/
      fXMLHttpRequest_send(this);
  };
  cXMLHttpRequest.prototype.abort = function() {
    // Add method sniffer
    if (cXMLHttpRequest.onabort)
      cXMLHttpRequest.onabort.apply(this, arguments);

    // BUGFIX: Gecko - unnecessary DONE when aborting
    if (this.readyState > cXMLHttpRequest.UNSENT)
      this._aborted = true;

    this._object.abort();

    // BUGFIX: IE - memory leak
    fCleanTransport(this);

    this.readyState = cXMLHttpRequest.UNSENT;

    delete this._data;
/*    if (this._async)
      fQueue_remove(this);*/
  };
  cXMLHttpRequest.prototype.getAllResponseHeaders = function() {
    return this._object.getAllResponseHeaders();
  };
  cXMLHttpRequest.prototype.getResponseHeader = function(sName) {
    return this._object.getResponseHeader(sName);
  };
  cXMLHttpRequest.prototype.setRequestHeader  = function(sName, sValue) {
    // BUGFIX: IE - cache issue
    if (!this._headers)
      this._headers = {};
    this._headers[sName]  = sValue;

    return this._object.setRequestHeader(sName, sValue);
  };

  // EventTarget interface implementation
  cXMLHttpRequest.prototype.addEventListener  = function(sName, fHandler, bUseCapture) {
    for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++)
      if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture)
        return;
    // Add listener
    this._listeners.push([sName, fHandler, bUseCapture]);
  };

  cXMLHttpRequest.prototype.removeEventListener = function(sName, fHandler, bUseCapture) {
    for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++)
      if (oListener[0] == sName && oListener[1] == fHandler && oListener[2] == bUseCapture)
        break;
    // Remove listener
    if (oListener)
      this._listeners.splice(nIndex, 1);
  };

  cXMLHttpRequest.prototype.dispatchEvent = function(oEvent) {
    var oEventPseudo  = {
      'type':     oEvent.type,
      'target':   this,
      'currentTarget':this,
      'eventPhase': 2,
      'bubbles':    oEvent.bubbles,
      'cancelable': oEvent.cancelable,
      'timeStamp':  oEvent.timeStamp,
      'stopPropagation':  function() {},  // There is no flow
      'preventDefault': function() {},  // There is no default action
      'initEvent':    function() {} // Original event object should be initialized
    };

    // Execute onreadystatechange
    if (oEventPseudo.type == "readystatechange" && this.onreadystatechange)
      (this.onreadystatechange.handleEvent || this.onreadystatechange).apply(this, [oEventPseudo]);

    // Execute listeners
    for (var nIndex = 0, oListener; oListener = this._listeners[nIndex]; nIndex++)
      if (oListener[0] == oEventPseudo.type && !oListener[2])
        (oListener[1].handleEvent || oListener[1]).apply(this, [oEventPseudo]);
  };

  //
  cXMLHttpRequest.prototype.toString  = function() {
    return '[' + "object" + ' ' + "XMLHttpRequest" + ']';
  };

  cXMLHttpRequest.toString  = function() {
    return '[' + "XMLHttpRequest" + ']';
  };

  // Helper function
  function fReadyStateChange(oRequest) {
    // Sniffing code
    if (cXMLHttpRequest.onreadystatechange)
      cXMLHttpRequest.onreadystatechange.apply(oRequest);

    // Fake event
    oRequest.dispatchEvent({
      'type':     "readystatechange",
      'bubbles':    false,
      'cancelable': false,
      'timeStamp':  new Date + 0
    });
  };

  function fGetDocument(oRequest) {
    var oDocument = oRequest.responseXML,
      sResponse = oRequest.responseText;
    // Try parsing responseText
    if (bIE && sResponse && oDocument && !oDocument.documentElement && oRequest.getResponseHeader("Content-Type").match(/[^\/]+\/[^\+]+\+xml/)) {
      oDocument = new window.ActiveXObject("Microsoft.XMLDOM");
      oDocument.async       = false;
      oDocument.validateOnParse = false;
      oDocument.loadXML(sResponse);
    }
    // Check if there is no error in document
    if (oDocument)
      if ((bIE && oDocument.parseError != 0) || !oDocument.documentElement || (oDocument.documentElement && oDocument.documentElement.tagName == "parsererror"))
        return null;
    return oDocument;
  };

  function fSynchronizeValues(oRequest) {
    try { oRequest.responseText = oRequest._object.responseText;  } catch (e) {}
    try { oRequest.responseXML  = fGetDocument(oRequest._object); } catch (e) {}
    try { oRequest.status     = oRequest._object.status;      } catch (e) {}
    try { oRequest.statusText   = oRequest._object.statusText;    } catch (e) {}
  };

  function fCleanTransport(oRequest) {
    // BUGFIX: IE - memory leak (on-page leak)
    oRequest._object.onreadystatechange = new window.Function;
  };
/*
  // Queue manager
  var oQueuePending = {"CRITICAL":[],"HIGH":[],"NORMAL":[],"LOW":[],"LOWEST":[]},
    aQueueRunning = [];
  function fQueue_add(oRequest) {
    oQueuePending[oRequest.priority in oQueuePending ? oRequest.priority : "NORMAL"].push(oRequest);
    //
    setTimeout(fQueue_process);
  };

  function fQueue_remove(oRequest) {
    for (var nIndex = 0, bFound = false; nIndex < aQueueRunning.length; nIndex++)
      if (bFound)
        aQueueRunning[nIndex - 1] = aQueueRunning[nIndex];
      else
      if (aQueueRunning[nIndex] == oRequest)
        bFound  = true;
    if (bFound)
      aQueueRunning.length--;
    //
    setTimeout(fQueue_process);
  };

  function fQueue_process() {
    if (aQueueRunning.length < 6) {
      for (var sPriority in oQueuePending) {
        if (oQueuePending[sPriority].length) {
          var oRequest  = oQueuePending[sPriority][0];
          oQueuePending[sPriority]  = oQueuePending[sPriority].slice(1);
          //
          aQueueRunning.push(oRequest);
          // Send request
          fXMLHttpRequest_send(oRequest);
          break;
        }
      }
    }
  };
*/
  // Internet Explorer 5.0 (missing apply)
  if (!window.Function.prototype.apply) {
    window.Function.prototype.apply = function(oRequest, oArguments) {
      if (!oArguments)
        oArguments  = [];
      oRequest.__func = this;
      oRequest.__func(oArguments[0], oArguments[1], oArguments[2], oArguments[3], oArguments[4]);
      delete oRequest.__func;
    };
  };

  // Register new object with window
  window.XMLHttpRequest = cXMLHttpRequest;
})();